#! /usr/local/bin/python
#-*- coding: utf-8 -*-

"""Interface graphique du démineur
"""

"""
Minesweeper 3K. Original implementation of the classic game of mines
for Python 3.
Copyright (C) 2010  Cedric Bonhomme

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

Minesweeper 3K  Copyright (C) 2010  Cedric Bonhomme
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
"""

__author__ = "Jerome Hussenet, Cedric Bonhomme"
__date__ = "$Date: 2010/02/27 $"
__copyright__ = "Copyright (c) 2010 Jerome Hussenet, Copyright (c) 2010 Cedric Bonhomme"
__license__ = "GPL 3"

from tkinter import *
import threading
import tkinter.messagebox
import time

from grille_graphique import *
from grille_jeu import *

import score
import gui_score_chargement
import gui_score_sauvegarde
import gui_partie_chargement
import gui_partie_sauvegarde
try:
    import tuxdroid
except Exception as e:
    print(e)

class Demineur_gui(object):
    """Classe graphique du démineur.
    """
    def __init__(self, w, h, m):
        """Initialisation de l'interface graphique.

        Cette classe contient le canvas ainsi que tous les composants graphiques du jeu.
        """
        self.master = Tk()
        self.master.title(_("Démineur"))
        #self.master.minsize(width = 220, height = 40)
        self.master.resizable(height = False, width = False)

        self.largeur = w
        self.hauteur = h
        self.nbmines = m

        # niveaux de jeux
        self.niveaux = {'debutant': (10, 10, 10),
                'intermediaire': (20, 20, 40),
                'expert' : (20, 20, 100),
                'perso' : (10, 10, 10)
                }
        self.niveau = 'debutant' # niveau par défaut

        # Thread du timer
        self.th = threading.Thread(None, self.affichetemps, None, (), {})
        self.th._stopevent = threading.Event()
        self.th.setDaemon(True)

        self.temps = 0
        self.tp_debut = 0
        self.fini = False
        self.perdu = False

        self.canvas = None
        self.grille_jeux = None



        # Chargement des styles
        self.style = StringVar() # contient le style selectionne via le menu <<Style>>.
        self.styles = style.Style() # objet style qui nous permettra d'effectuer les changements de styles
        self.listeStyle = self.styles.listeStyle # liste des style trouvés

        if not self.listeStyle:
            print(_("Le jeu ne peut pas se construire :\nAucun style de jeu trouvé.\n"))
            tkinter.messagebox.showerror('Erreur',\
                _("Le jeu ne peut pas se construire :\nAucun style de jeu trouvé.\n"))
            self.master.quit()
            return

        self.leStyle = "gnome" # style par défaut
        if self.leStyle not in [liste[0] for liste in self.listeStyle]:
            self.leStyle = self.listeStyle[0][0]
        self.styles.chargeStyle(self.leStyle)

        print(_("Style(s) trouvé(s) :"))
        for i in self.listeStyle:
            print("\t", i[1])
        print(_("\nStyle par défaut :"), self.leStyle)



        # Connexion avec Tux Droid
        self.tuxy = None
        self.tuxy_thread = threading.Thread(None, self.connect_tux, None, (), {})
        self.tuxy_thread.setDaemon(True)
        self.tuxy_thread.start()


        # Creation du menu Principal
        self.menu=Menu(self.master)
        self.master.config(menu=self.menu)
        # Menu Partie
        filemenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = _("Partie"), menu = filemenu)
        filemenu.add_command(label = _("Nouveau"), command = self.nouveau)
        filemenu.add_separator()
        niveau = Menu(filemenu, tearoff = 0)
        filemenu.add_cascade(label = _("Niveau"), menu = niveau)
        niveau.add_command(label = _("Débutant"), command = lambda : self.changeNiveau(('debutant')))
        niveau.add_command(label = _("Intermédiaire"), command = lambda : self.changeNiveau(('intermediaire')))
        niveau.add_command(label = _("Expert"), command = lambda : self.changeNiveau(('expert')))
        niveau.add_separator()
        niveau.add_command(label = _("Personnalisé"), command = self.niveauPerso)
        filemenu.add_separator()
        filemenu.add_command(label = _("Sauvegarder"), command = self.sauvegarder)
        filemenu.add_command(label = _("Charger"), command = self.charger)
        filemenu.add_separator()
        filemenu.add_command(label = _("Scores"), command = self.score)
        filemenu.add_separator()
        filemenu.add_command(label = _("Quitter"), command = self.quitter)
        # Menu Statistiques
        statmenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = _("Statistiques"), menu = statmenu)
        niveau = Menu(statmenu, tearoff = 0)
        statmenu.add_cascade(label = _("Histogramme"), menu = niveau)
        niveau.add_command(label = _("Débutant"), command = lambda : self.histogramme(('debutant')))
        niveau.add_command(label = _("Intermédiaire"), command = lambda : self.histogramme(('intermediaire')))
        niveau.add_command(label = _("Expert"), command = lambda : self.histogramme(('expert')))
        # Menu dynamique Style
        stylemenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = _("Style"), menu = stylemenu)
        for stile in self.listeStyle:
            stylemenu.add_radiobutton(label = stile[1], value = (stile[0]) , \
                                variable = self.style, command = self.changeStyle)
        # Menu Aide
        helpmenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = _("Aide"), menu = helpmenu)
        helpmenu.add_command(label = _("À propos"), command = self.about)


        # Indicateur de drapeaux, smiley et nombre de mines
        self.dra = Label(self.master, text = str(self.nbmines), padx = 5, pady = 5, **self.styles.textStyle)
        self.dra.grid(row = 0, column = 0, sticky = W)

        self.smiley = Button (self.master, padx = 5, pady = 5, text = "X", image = self.styles.cool, command = self.nouveau)
        self.smiley.grid(row = 0, column = 1)

        self.score = Label(self.master, padx = 5, pady = 5, text="00", **self.styles.textStyle)
        self.score.grid(row = 0, column = 2, sticky = E)

        # Bind pour détecter le focus de l'application
        self.master.bind("<FocusOut>", self.lostFocus)
        self.master.bind("<FocusIn>", self.reFocus)

        # Lancement de l'interface
        self.master.mainloop()

    def connect_tux(self):
        """Lance la connexion avec Tux Droid.

        Appelée dans un thread.
        """
        try:
            self.tuxy = tuxdroid.Tux() # instruction bloquante
        except NameError:
            # module tuxisalive non insallé
            # erreur déjà rapporté
            pass
        except Exception as e:
            print(e)
            self.tuxy = None

    def lostFocus(self, event):
        """Met l'interface en pause.

        Remplace les cases par une image de pause lors de la perte du focus.
        """
        try:
            if self.tp_debut != 0 and self.master.focus_get() != self.master \
                and self.fini == False:
                self.master.title(_("Démineur - En pause"))
                if (not self.canvas == None):
                    c = self.canvas.find_all()
                    for i in c:
                        self.canvas.delete(i)
                    self.canvas.delete
                self.canvas.configure(cursor="coffee_mug")
                self.canvas.create_image(int(self.grille_jeux.larg*self.largeur / 2) - 90, \
                    int(self.grille_jeux.larg*self.hauteur / 2) - 90, image = self.styles.pause, anchor = NW)
        except: # exception générée à l'ouverture d'un menu, on continue normalement.
                pass

    def reFocus(self, event):
        """Sort l'interface de la pause.

        Recrée le canvas tel qu'il était avant la mise ne pause lorsque l'application récupère le focus.
        """
        if self.tp_debut != 0 and self.fini == False:
            self.master.title(_("Démineur"))
            c = self.canvas.find_all()
            for i in c:
                self.canvas.delete(i)
            self.canvas.delete
            self.canvas.configure(cursor=self.styles.curseur)
            self.grille_jeux.redessiner()

    def nouveau(self):
        """Lance une nouvelle partie.

        Réinitialise la grille, les autres composants, le temps, etc.
        """
        try:
            self.grille = grille(self.largeur, self.hauteur, self.nbmines)
        except ValueError as erreur:
            tkinter.messagebox.showerror(_('Erreur'), erreur)
            self.niveau = 'debutant'
            self.changeNiveau(self.niveau)
            return

        if (not self.canvas == None):
            c = self.canvas.find_all()
            for i in c:
                self.canvas.delete(i)
            self.canvas.delete
        else:
            self.canvas = Canvas(self.master, width=self.styles.largCanevas*self.largeur, \
                    height=self.styles.largCanevas*self.hauteur)


        self.fini = False
        self.perdu = False

        self.grille_jeux = grille_jeux(self, self.canvas, self.grille, self.styles)
        self.canvas.configure(width=self.styles.largCanevas*self.largeur, \
                height=self.styles.largCanevas*self.hauteur, cursor=self.styles.curseur)


        self.dra.configure(text=str(self.nbmines))
        self.relanceTemps()
        #print "debut: "+ str(int(self.tp_debut))
        self.smiley.configure(image = self.styles.cool)
        if self.styles.leStyle == "romain":
            self.dra.configure(text=roman.toRoman(self.nbmines), **self.styles.textStyle)
            self.score.configure(text=roman.toRoman(int(self.temps)), **self.styles.textStyle)
        else:
            self.dra.configure(text=str(self.nbmines), **self.styles.textStyle)
            self.score.configure(text=str(int(self.temps)), **self.styles.textStyle)

        self.canvas.grid(row=1, column=0, columnspan=3)

        self.tp_debut = time.time()
        self.temps = 0
        self.relanceTemps()

        if self.tuxy is not None: # si tux droid connecté
            self.tuxy.say_something("Nouvelle partie.")

    def rechargerPartie(self, temps, niveau, w, h, m, le, lm):
        """Recharge une partie préalablement sauvegardée.
        """
        self.largeur = w
        self.hauteur = h
        self.nbmines = m
        self.niveau = niveau

        self.fini = False
        self.perdu = False

        #on reinitialise le canvas
        if (not self.canvas == None):
            c = self.canvas.find_all()
            for i in c:
                self.canvas.delete(i)
            self.canvas.delete
        else:
            self.canvas = Canvas(self.master, width=self.styles.largCanevas*self.largeur, \
                    height=self.styles.largCanevas*self.hauteur)

        try:
            self.grille = grille(w,h,m)
        except:
            pass
        self.grille.chargerGrille(w, h, m, lm)
        print(self.grille)

        self.grille_jeux = grille_jeux(self, self.canvas, self.grille, self.styles)
        self.grille_jeux.charger(le)

        self.smiley.configure(image = self.styles.cool)
        self.dra.configure(text=str(self.nbmines - self.grille_jeux.nb_drapeau))

        if self.styles.leStyle == "romain":
            self.dra.configure(text=roman.toRoman(self.nbmines - self.grille_jeux.nb_drapeau))
        else:
            self.dra.configure(text=str(self.nbmines - self.grille_jeux.nb_drapeau))

        self.canvas.configure(width=self.grille_jeux.larg*self.largeur, \
                height=self.grille_jeux.larg*self.hauteur, cursor=self.styles.curseur)
        self.canvas.grid(row=1, column=0, columnspan=3)

        self.tp_debut = time.time() - temps
        self.temps = temps
        self.relanceTemps()

    def affichetemps(self):
        """Affiche le temps écoulé.

        Fonction lancée dans le thread self.th
        """
        diff= 0
        while not self.th._stopevent.isSet():
            try:

                if self.master.focus_get() == self.master: # si la fenêtre à le focus
                    self.temps = time.time() - self.tp_debut + diff

                    if self.styles.leStyle == "romain": # si style romain, on affiche le temps en chiffres romains
                        self.score.configure(text=roman.toRoman(int(self.temps)))
                    else:
                        self.score.configure(text=str(int(self.temps)))

                elif self.master.focus_get() != self.master:
                    # lors de la pause on calcul la _différence_ de temps
                    diff = self.temps - (time.time() - self.tp_debut)

            except: # exception générée à l'ouverture d'un menu, on continue normalement.
                    self.temps = time.time() - self.tp_debut + diff

                    if self.styles.leStyle == "romain":
                        self.score.configure(text=roman.toRoman(int(self.temps)))
                    else:
                        self.score.configure(text=str(int(self.temps)))

            self.th._stopevent.wait(0.08) # laisse le temps au CPU de respirer

    def relanceTemps(self):
        """Relance le temps.

        Lance ou relance le thread pour une nouvelle partie.
        """
        self.fini = False
        try:
            self.th.start()
        except:  # une partie a déjà été initiée
            self.th._stopevent.set()
            time.sleep(0.02)
            del self.th
            self.th = threading.Thread(None, self.affichetemps, None, (), {})
            self.th.setDaemon(True)
            self.th._stopevent = threading.Event()
            self.th.start()

    def sauvegarder(self):
        """Sauvegarder une partie.

        Lance la fenêtre de sauvegarde uniquement si une partie est en cours.
        """
        if self.tp_debut != 0 and self.fini == False:
            sauv = gui_partie_sauvegarde.SauvegardeGui(self.master, self.grille_jeux.grille_etat, self.grille.c, \
                self.nbmines, self.largeur, self.hauteur, int(self.temps), self.niveau)
        else:
            tkinter.messagebox.showerror(_('Erreur'), _("Aucune partie en cours."))

    def charger(self):
        """Lance la gui de chargement de parties.
        """
        char = gui_partie_chargement.ChargementGui(self)

    def score(self):
        """Lance la gui pour consulter les scores.
        """
        score = gui_score_chargement.Score(self.master)

    def quitter(self):
        """Stoppe le thread et quitte le jeu.
        """
        self.th._stopevent.wait(0.5)
        self.th._stopevent.set()
        del self.th
        self.master.destroy()

    def changeStyle(self):
        """Charge le style sélectionné.
        """
        if self.leStyle != self.style.get():
            self.leStyle = self.style.get()
            print(_("Nouveau style :"), self.leStyle)
            self.styles.chargeStyle(self.leStyle)

            if not self.grille_jeux == None:
                self.canvas.configure(width=self.styles.largCanevas*self.largeur, \
                        height=self.styles.largCanevas*self.hauteur, cursor=self.styles.curseur)
                self.grille_jeux.canvas = self.canvas
                self.grille_jeux.changerStyle(self.leStyle)
            else:
                self.smiley.configure(image = self.styles.cool)
                if self.styles.leStyle == "romain":
                    self.dra.configure(text=roman.toRoman(self.nbmines), **self.styles.textStyle)
                    self.score.configure(text=roman.toRoman(int(self.temps)), **self.styles.textStyle)
                else:
                    self.dra.configure(text=str(self.nbmines), **self.styles.textStyle)
                    self.score.configure(text=str(int(self.temps)), **self.styles.textStyle)

            if self.tuxy is not None: # si tux droid connecté
                self.tuxy.say_something("Changement de style.")

    def changeNiveau(self, niveau):
        """Change le niveau de Jeu.

        Si une partie est déjà en cours on en relance une nouvelle.
        """
        self.niveau = niveau
        self.hauteur, self.largeur, self.nbmines = self.niveaux[self.niveau]
        if self.tp_debut != 0 and self.fini == False:
            self.th._stopevent.set()
            self.fini = True
            self.nouveau()
        if self.tuxy is not None: # si tux droid connecté
            time.sleep(0.9) # laisse le temps de finir la phrase précédente
            self.tuxy.say_something("Niveau" + niveau)

    def niveauPerso(self):
        """MessageBox pour le niveau personnalisé.

        Permet au joueur de personnaliser un niveau.
        """
        def execute():
            try:
                lignes = int(nbLineEntry.get())
                colonnes = int(nbColomnEntry.get())
                mines = int(nbMineEntry.get())
            except ValueError:
                tkinter.messagebox.showerror(_('Erreur'), _('Entrée incorrecte.'))
                return

            for var in (lignes, colonnes):
                if var < 10 or var > 40 :
                    tkinter.messagebox.showerror(_('Erreur'), _('Les nombres de lignes et de colonnes doivent être entre 10 et 40.'))
                    nbLineEntry.delete(0, END)
                    nbColomnEntry.delete(0, END)
                    nbMineEntry.delete(0, END)
                    return
            borneMin = int(((lignes * colonnes) * 10 / 100))
            borneMax = int(((lignes * colonnes) * 30 / 100))
            if mines < borneMin or mines > borneMax:
                nbMineEntry.delete(0, END)
                tkinter.messagebox.showerror(_('Erreur'), \
                    _('Le nombre de mines doit être entre 10% et 30% du nombre de cases.\n')+
                    _('Ici entre ') + str(borneMin) + _(' et ') + str(borneMax) + '.')
            else:
                self.niveau = 'perso'
                self.niveaux[self.niveau] = lignes, colonnes, mines
                self.changeNiveau(self.niveau)
                msg.destroy()


        msg = Toplevel(self.master)
        msg.title(_('Personnaliser un jeux'))
        msg.resizable(height = False, width = False)

        Label(msg, text=_('Lignes : ')).grid(row = 0, column = 0)
        nbLineEntry = Entry(msg)
        nbLineEntry.grid(row = 0, column = 1, pady = 2)
        nbLineEntry.focus_set()

        Label(msg, text=_('Colonnes : ')).grid(row = 1, column = 0)
        nbColomnEntry = Entry(msg)
        nbColomnEntry.grid(row = 1, column = 1, pady = 2)

        Label(msg, text=_('Mines : ')).grid(row = 2, column = 0)
        nbMineEntry = Entry(msg)
        nbMineEntry.grid(row = 2, column = 1, pady = 2)

        Button(msg, text=_("OK"), \
            command = execute).grid(row = 3, columnspan = 2, pady = 2)

        ## fixe le focus sur la fenêtre (obligatoire pour Windows)
        msg.focus_set()
        ## Force les évènements à aller sur cette fenêtre
        msg.grab_set()
        ## Pour que la fenêtre reste au-dessus de sa fenêtre parente
        msg.transient(self.master)
        ## Affiche la fenêtre et attend sa fermeture
        msg.wait_window(msg)

    def terminer(self, perdu):
        """Termine la partie.

        Fonction appelée lorque le joueur a gagné ou perdu.
        S'il a gagné et qu'il est "élligible",
        on lui propose de faire partie des meilleurs scores.
        """
        self.th._stopevent.set()
        self.fini = True
        if perdu:
            print(_("Perdu :-("))
            self.perdu = True
            self.smiley.configure(image = self.styles.perdu)
            if self.tuxy is not None: # si tux droid connecté
                self.tuxy.loose()
        else:
            print(_("Gagné :-)"))
            self.smiley.configure(image = self.styles.gagne)
            if self.tuxy is not None:  # si tux droid connecté
                self.tuxy.win()
            if self.niveau != 'perso': # on enregistre pas les scores des niveaux perso.
                lesScores = score.Score()
                if lesScores.testSiAjout(self.niveau, int(self.temps)): # s'il le score est un highscore.
                    lescore = gui_score_sauvegarde.SauvegardeGui(int(self.temps), self.niveau, self.master)
        self.tp_debut = 0

    def histogramme(self, niveau):
        """Ouvre la fenêtre contenant l'histogramme.
        """
        import stats
        stats.trace(niveau)

    def about(self):
        """Fenêtre-message à propos.

        Indique le nom des auteurs ainsi que la licence.
        """
        message = "Minesweeper"+"\n\nCopyright (C) 2008\n\nJérôme Hussenet\nCédric Bonhomme\n" \
        "\nsites.google.com/site/pyminesweeper/\n" \
        "\nLicense : GPL Version 3, 29 June 2007"

        if self.tuxy is not None: # si tux droid connecté
            self.tuxy.say_something("Jeu du demineur. \
                                    Allez sur le site pour plus d'informations.")

        print("\n" + message + "\n")

        msg = Toplevel(self.master)
        msg.title(_('À propos'))
        msg.resizable(height = False, width = False)
        Message(msg, width = 300, aspect = 100, justify = CENTER, \
        text = message).pack(padx = 10, pady = 10)

        ## fixe le focus sur la fenêtre (obligatoire pour Windows)
        msg.focus_set()
        ## Force les évènements à aller sur cette fenêtre
        msg.grab_set()
        ## Pour que la fenêtre reste au-dessus de sa fenêtre parente
        msg.transient(self.master)
        ## Affiche la fenêtre et attend sa fermeture
        msg.wait_window(msg)

if __name__ == '__main__':
    # Point d'entrée en mode exécution.
    import gettext, os
    pathname = os.path.dirname(sys.argv[0])
    localdir = os.path.abspath(pathname) + "/locale"
    gettext.install("messages", localdir)
    Demineur_gui(10, 10, 10)